#include "qjshighlight.h"

#include <math.h>
#include <QTimer>

static bool byStartOfRange(const QTextLayout::FormatRange &range, const QTextLayout::FormatRange &other)
{
    return range.start < other.start;
}


static bool adjustRange(QTextLayout::FormatRange &range, int from, int charsRemoved, int charsAdded) {

    if (range.start >= from) {
        range.start += charsAdded - charsRemoved;
        return true;
    } else if (range.start + range.length > from) {
        range.length += charsAdded - charsRemoved;
        return true;
    }
    return false;
}

QJSHighLight::QJSHighLight(QTextDocument *parent) :
    QObject(parent),
    m_doc(parent),
    m_rehighlightPending(false),
    m_inReformatBlocks(false),
    m_inMultilineComment(false),
    m_braceDepth(0),
    m_foldingIndent(0)
{
    if (m_doc)
    {
        connect(m_doc, SIGNAL(contentsChange(int,int,int)),
                this, SLOT(reformatBlocks_slot(int,int,int)));
        m_rehighlightPending = true;
        QTimer::singleShot(0, this, SLOT(delayedRehighlight()));
        m_foldValidator.setup(qobject_cast<BaseTextDocumentLayout *>(m_doc->documentLayout()));
    }

    m_currentBlockParentheses.reserve(20);

    m_formats[NumberFormat].setForeground(Qt::darkBlue);
    m_formats[StringFormat].setForeground(Qt::darkGreen);
    m_formats[TypeFormat].setForeground(Qt::darkMagenta);
    m_formats[KeywordFormat].setForeground(Qt::darkYellow);
    m_formats[FieldFormat].setForeground(Qt::darkRed);
    m_formats[CommentFormat].setForeground(Qt::darkGreen);
    m_formats[VisualWhitespace].setForeground(Qt::lightGray);
}

QTextDocument* QJSHighLight::document()const
{
    return m_doc;
}

void QJSHighLight::highlightBlock(const QString &text)
{
    const QList<Token> tokens = m_scanner(text, onBlockStart());

    int index = 0;
    while (index < tokens.size())
    {
        const Token &token = tokens.at(index);

        switch (token.kind)
        {
        case Token::Keyword:
            setFormat(token.offset, token.length, m_formats[KeywordFormat]);
            break;
        case Token::String:
            setFormat(token.offset, token.length, m_formats[StringFormat]);
            break;
        case Token::Comment:
            if (m_inMultilineComment && text.midRef(token.end() - 2, 2) == QLatin1String("*/"))
            {
                onClosingParenthesis(QLatin1Char('-'), token.end() - 1, index == tokens.size()-1);
                m_inMultilineComment = false;
            } else if (!m_inMultilineComment
                       && (m_scanner.state() & Scanner::MultiLineMask) == Scanner::MultiLineComment
                       && index == tokens.size() - 1)
            {
                onOpeningParenthesis(QLatin1Char('+'), token.offset, index == 0);
                m_inMultilineComment = true;
            }
            setFormat(token.offset, token.length, m_formats[CommentFormat]);
            break;
        case Token::RegExp:
            setFormat(token.offset, token.length, m_formats[StringFormat]);
            break;
        case Token::LeftParenthesis:
            onOpeningParenthesis(QLatin1Char('('), token.offset, index == 0);
            break;
        case Token::RightParenthesis:
            onClosingParenthesis(QLatin1Char(')'), token.offset, index == tokens.size()-1);
            break;
        case Token::LeftBrace:
            onOpeningParenthesis(QLatin1Char('{'), token.offset, index == 0);
            break;
        case Token::RightBrace:
            onClosingParenthesis(QLatin1Char('}'), token.offset, index == tokens.size()-1);
            break;
        case Token::LeftBracket:
            onOpeningParenthesis(QLatin1Char('['), token.offset, index == 0);
            break;
        case Token::RightBracket:
            onClosingParenthesis(QLatin1Char(']'), token.offset, index == tokens.size()-1);
            break;
        case Token::Identifier:
            break;
        case Token::Delimiter:
            break;
        default:
            break;
        }
        ++index;
    }

    int previousTokenEnd = 0;
    for (int index = 0; index < tokens.size(); ++index)
    {
        const Token &token = tokens.at(index);
        setFormat(previousTokenEnd, token.begin() - previousTokenEnd, m_formats[VisualWhitespace]);
        switch (token.kind)
        {
        case Token::Comment:
        case Token::String:
        case Token::RegExp:
        {
            int i = token.begin(), e = token.end();
            while (i < e)
            {
                const QChar ch = text.at(i);
                if (ch.isSpace())
                {
                    const int start = i;
                    do
                    {
                        ++i;
                    } while (i < e && text.at(i).isSpace());
                    setFormat(start, i - start, m_formats[VisualWhitespace]);
                } else {
                    ++i;
                }
            }
        } break;

        default:
            break;
        }

        previousTokenEnd = token.end();
    }

    setFormat(previousTokenEnd, text.length() - previousTokenEnd, m_formats[VisualWhitespace]);
    setCurrentBlockState(m_scanner.state());
    onBlockEnd(m_scanner.state());
}

int QJSHighLight::onBlockStart()
{
    m_currentBlockParentheses.clear();
    m_braceDepth = 0;
    m_foldingIndent = 0;
    m_inMultilineComment = false;

    if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) {
        userData->setFoldingIndent(0);
        userData->setFoldingStartIncluded(false);
        userData->setFoldingEndIncluded(false);
    }

    int state = 0;
    int previousState = previousBlockState();
    if (previousState != -1)
    {
        state = previousState & 0xff;
        m_braceDepth = (previousState >> 8);
        m_inMultilineComment = ((state & Scanner::MultiLineMask) == Scanner::MultiLineComment);
    }
    m_foldingIndent = m_braceDepth;
    return state;
}

void QJSHighLight::onBlockEnd(int state)
{
    setCurrentBlockState((m_braceDepth << 8) | state);
    BaseTextDocumentLayout::setFoldingIndent(currentBlock(), m_foldingIndent);
    BaseTextDocumentLayout::setParentheses(currentBlock(), m_currentBlockParentheses);
}

void QJSHighLight::onOpeningParenthesis(QChar parenthesis, int pos, bool atStart)
{
    if (parenthesis == QLatin1Char('{') || parenthesis == QLatin1Char('[') || parenthesis == QLatin1Char('+'))
    {
        ++m_braceDepth;
        if (atStart)
        {
            BaseTextDocumentLayout::userData(currentBlock())->setFoldingStartIncluded(true);
        }

    }
    m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Opened, parenthesis, pos));
}

void QJSHighLight::onClosingParenthesis(QChar parenthesis, int pos, bool atEnd)
{
    if (parenthesis == QLatin1Char('}') || parenthesis == QLatin1Char(']') || parenthesis == QLatin1Char('-'))
    {
        --m_braceDepth;
        if (atEnd)
            BaseTextDocumentLayout::userData(currentBlock())->setFoldingEndIncluded(true);
        else
            m_foldingIndent = qMin(m_braceDepth, m_foldingIndent);
    }
    m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Closed, parenthesis, pos));
}

void QJSHighLight::setExtraAdditionalFormats(const QTextBlock& block,
                                                  QList<QTextLayout::FormatRange> &formats)
{
    const int blockLength = block.length();
    if (block.layout() == 0 || blockLength == 0)
    {
        return;
    }

    qSort(formats.begin(), formats.end(), byStartOfRange);

    const QList<QTextLayout::FormatRange> all = block.layout()->additionalFormats();
    QList<QTextLayout::FormatRange> previousSemanticFormats;
    QList<QTextLayout::FormatRange> formatsToApply;
    previousSemanticFormats.reserve(all.size());
    formatsToApply.reserve(all.size() + formats.size());

    for (int i = 0, ei = formats.size(); i < ei; ++i)
    {
        formats[i].format.setProperty(QTextFormat::UserProperty, true);
    }

    foreach (const QTextLayout::FormatRange &r, all)
    {
        if (r.format.hasProperty(QTextFormat::UserProperty))
        {
            previousSemanticFormats.append(r);
        }
        else
        {
            formatsToApply.append(r);
        }
    }

    if (formats.size() == previousSemanticFormats.size())
    {
        qSort(previousSemanticFormats.begin(), previousSemanticFormats.end(), byStartOfRange);

        int index = 0;
        for (; index != formats.size(); ++index) {
            const QTextLayout::FormatRange &range = formats.at(index);
            const QTextLayout::FormatRange &previousRange = previousSemanticFormats.at(index);

            if (range.start != previousRange.start ||
                    range.length != previousRange.length ||
                    range.format != previousRange.format)
                break;
        }

        if (index == formats.size())
            return;
    }

    formatsToApply += formats;

    bool wasInReformatBlocks = m_inReformatBlocks;
    m_inReformatBlocks = true;
    block.layout()->setAdditionalFormats(formatsToApply);
    document()->markContentsDirty(block.position(), blockLength - 1);
    m_inReformatBlocks = wasInReformatBlocks;
}

void QJSHighLight::rehighlight(QTextCursor &cursor, QTextCursor::MoveOperation operation)
{
    m_inReformatBlocks = true;
    cursor.beginEditBlock();
    int from = cursor.position();
    cursor.movePosition(operation);
    reformatBlocks(from, 0, cursor.position() - from);
    cursor.endEditBlock();
    m_inReformatBlocks = false;
}

void QJSHighLight::reformatBlocks_slot(int from, int charsRemoved, int charsAdded)
{
    if (!m_inReformatBlocks)
        reformatBlocks(from, charsRemoved, charsAdded);
}

void QJSHighLight::reformatBlocks(int from, int charsRemoved, int charsAdded)
{
    m_foldValidator.reset();

    m_rehighlightPending = false;

    QTextBlock block = m_doc->findBlock(from);
    if (!block.isValid())
    {
        return;
    }

    int endPosition;
    QTextBlock lastBlock = m_doc->findBlock(from + charsAdded + (charsRemoved > 0 ? 1 : 0));
    if (lastBlock.isValid())
    {
        endPosition = lastBlock.position() + lastBlock.length();
    }
    else
    {
        endPosition =  m_doc->lastBlock().position() + m_doc->lastBlock().length(); //doc->docHandle()->length();
    }

    bool forceHighlightOfNextBlock = false;

    while (block.isValid() && (block.position() < endPosition || forceHighlightOfNextBlock))
    {
        const int stateBeforeHighlight = block.userState();

        reformatBlock(block, from, charsRemoved, charsAdded);

        forceHighlightOfNextBlock = (block.userState() != stateBeforeHighlight);

        block = block.next();
    }

    m_formatChanges.clear();

    m_foldValidator.finalize();
}

void QJSHighLight::reformatBlock(const QTextBlock &block, int from, int charsRemoved, int charsAdded)
{
    m_currentBlock = block;

    m_formatChanges.fill(QTextCharFormat(), block.length() - 1);
    highlightBlock(block.text());
    applyFormatChanges(from, charsRemoved, charsAdded);

    m_foldValidator.process(m_currentBlock);

    m_currentBlock = QTextBlock();
}

void QJSHighLight::applyFormatChanges(int from, int charsRemoved, int charsAdded)
{
    bool formatsChanged = false;

    QTextLayout *layout = m_currentBlock.layout();

    QList<QTextLayout::FormatRange> ranges = layout->additionalFormats();

    bool doAdjustRange = m_currentBlock.contains(from);

    QList<QTextLayout::FormatRange> old_ranges;

    if (!ranges.isEmpty())
    {
        QList<QTextLayout::FormatRange>::Iterator it = ranges.begin();
        while (it != ranges.end()) {
            if (it->format.property(QTextFormat::UserProperty).toBool())
            {
                if (doAdjustRange)
                    formatsChanged = adjustRange(*it, from - m_currentBlock.position(), charsRemoved, charsAdded)
                            || formatsChanged;
                ++it;
            }
            else
            {
                old_ranges.append(*it);
                it = ranges.erase(it);
            }
        }
    }

    QTextCharFormat emptyFormat;

    QTextLayout::FormatRange r;
    r.start = -1;

    QList<QTextLayout::FormatRange> new_ranges;
    int i = 0;
    while (i < m_formatChanges.count())
    {

        while (i < m_formatChanges.count() && m_formatChanges.at(i) == emptyFormat)
            ++i;

        if (i >= m_formatChanges.count())
            break;

        r.start = i;
        r.format = m_formatChanges.at(i);

        while (i < m_formatChanges.count() && m_formatChanges.at(i) == r.format)
            ++i;

        if (i >= m_formatChanges.count())
            break;

        r.length = i - r.start;

        new_ranges << r;
        r.start = -1;
    }

    if (r.start != -1) {
        r.length = m_formatChanges.count() - r.start;

        new_ranges << r;
    }

    formatsChanged = formatsChanged || (new_ranges.size() != old_ranges.size());

    for (int i = 0; !formatsChanged && i < new_ranges.size(); ++i)
    {
        const QTextLayout::FormatRange &o = old_ranges.at(i);
        const QTextLayout::FormatRange &n = new_ranges.at(i);
        formatsChanged = (o.start != n.start || o.length != n.length || o.format != n.format);
    }

    if (formatsChanged)
    {
        ranges.append(new_ranges);
        layout->setAdditionalFormats(ranges);
        m_doc->markContentsDirty(m_currentBlock.position(), m_currentBlock.length());
    }
}

void QJSHighLight::rehighlight()
{
    QTextCursor cursor(m_doc);
    rehighlight(cursor, QTextCursor::End);
}

void QJSHighLight::delayedRehighlight()
{
    if (!m_rehighlightPending)
        return;
    m_rehighlightPending = false;
    rehighlight();
}

QList<QColor> QJSHighLight::generateColors(int n, const QColor &background)
{
    QList<QColor> result;
    // Assign a color gradient. Generate a sufficient number of colors
    // by using ceil and looping from 0..step.
    const double oneThird = 1.0 / 3.0;
    const int step = qRound(ceil(pow(double(n), oneThird)));
    result.reserve(step * step * step);
    const int factor = 255 / step;
    const int half = factor / 2;
    const int bgRed = background.red();
    const int bgGreen = background.green();
    const int bgBlue = background.blue();
    for (int r = step; r >= 0; --r) {
        const int red = r * factor;
        if (bgRed - half > red || bgRed + half <= red) {
            for (int g = step; g >= 0; --g) {
                const int green = g * factor;
                if (bgGreen - half > green || bgGreen + half <= green) {
                    for (int b = step; b >= 0 ; --b) {
                        const int blue = b * factor;
                        if (bgBlue - half > blue || bgBlue + half <= blue)
                            result.append(QColor(red, green, blue));
                    }
                }
            }
        }
    }
    return result;
}

void QJSHighLight::rehighlightBlock(const QTextBlock &block)
{
    if (!m_doc || !block.isValid() || block.document() != m_doc)
        return;

    const bool rehighlightPending = m_rehighlightPending;

    QTextCursor cursor(block);
    rehighlight(cursor, QTextCursor::EndOfBlock);

    if (rehighlightPending)
        m_rehighlightPending = rehighlightPending;
}

void QJSHighLight::setFormat(int start, int count, const QTextCharFormat &format)
{
    if (start < 0 || start >= m_formatChanges.count())
        return;

    const int end = qMin(start + count, m_formatChanges.count());
    for (int i = start; i < end; ++i)
        m_formatChanges[i] = format;
}

void QJSHighLight::setFormat(int start, int count, const QColor &color)
{
    QTextCharFormat format;
    format.setForeground(color);
    setFormat(start, count, format);
}

void QJSHighLight::setFormat(int start, int count, const QFont &font)
{
    QTextCharFormat format;
    format.setFont(font);
    setFormat(start, count, format);
}

QTextCharFormat QJSHighLight::format(int pos) const
{
    if (pos < 0 || pos >= m_formatChanges.count())
        return QTextCharFormat();
    return m_formatChanges.at(pos);
}
void QJSHighLight::applyFormatToSpaces(const QString &text, const QTextCharFormat &format)
{
    int offset = 0;
    const int length = text.length();
    while (offset < length)
    {
        if (text.at(offset).isSpace())
        {
            int start = offset++;
            while (offset < length && text.at(offset).isSpace())
                ++offset;
            setFormat(start, offset - start, format);
        } else {
            ++offset;
        }
    }
}

int QJSHighLight::previousBlockState() const
{
    if (!m_currentBlock.isValid())
        return -1;

    const QTextBlock previous = m_currentBlock.previous();
    if (!previous.isValid())
        return -1;

    return previous.userState();
}

int QJSHighLight::currentBlockState() const
{
    if (!m_currentBlock.isValid())
        return -1;

    return m_currentBlock.userState();
}

void QJSHighLight::setCurrentBlockState(int newState)
{
    if (!m_currentBlock.isValid())
        return;

    m_currentBlock.setUserState(newState);
}

void QJSHighLight::setCurrentBlockUserData(QTextBlockUserData *data)
{
    if (!m_currentBlock.isValid())
        return;

    m_currentBlock.setUserData(data);
}

QTextBlockUserData *QJSHighLight::currentBlockUserData() const
{
    if (!m_currentBlock.isValid())
        return 0;

    return m_currentBlock.userData();
}

QTextBlock QJSHighLight::currentBlock() const
{
    return m_currentBlock;
}
